﻿using System;
using System.Collections.Generic;
using System.Text;
using RoughJs.Ast;
using RoughJs.Error;

namespace RoughJs.E4X
{
    public class XMLName : Ref
    {
        private static bool isNCNameStartChar(int c)
        {
            if ((c & ~0x7F) == 0)
            {
                // Optimize for ASCII and use A..Z < _ < a..z
                if (c >= 'a')
                {
                    return c <= 'z';
                }
                else if (c >= 'A')
                {
                    if (c <= 'Z')
                    {
                        return true;
                    }
                    return c == '_';
                }
            }
            else if ((c & ~0x1FFF) == 0)
            {
                return (0xC0 <= c && c <= 0xD6)
                || (0xD8 <= c && c <= 0xF6)
                || (0xF8 <= c && c <= 0x2FF)
                || (0x370 <= c && c <= 0x37D)
                || 0x37F <= c;
            }
            return (0x200C <= c && c <= 0x200D)
            || (0x2070 <= c && c <= 0x218F)
            || (0x2C00 <= c && c <= 0x2FEF)
            || (0x3001 <= c && c <= 0xD7FF)
            || (0xF900 <= c && c <= 0xFDCF)
            || (0xFDF0 <= c && c <= 0xFFFD)
            || (0x10000 <= c && c <= 0xEFFFF);
        }

        private static bool isNCNameChar(int c)
        {
            if ((c & ~0x7F) == 0)
            {
                // Optimize for ASCII and use - < . < 0..9 < A..Z < _ < a..z
                if (c >= 'a')
                {
                    return c <= 'z';
                }
                else if (c >= 'A')
                {
                    if (c <= 'Z')
                    {
                        return true;
                    }
                    return c == '_';
                }
                else if (c >= '0')
                {
                    return c <= '9';
                }
                else
                {
                    return c == '-' || c == '.';
                }
            }
            else if ((c & ~0x1FFF) == 0)
            {
                return isNCNameStartChar(c) || c == 0xB7
                    || (0x300 <= c && c <= 0x36F);
            }
            return isNCNameStartChar(c) || (0x203F <= c && c <= 0x2040);
        }

        //    This means "accept" in the parsing sense
        //    See ECMA357 13.1.2.1
        public static bool accept(Object nameObj)
        {
            String name;
            try
            {
                name = ScriptRuntime.toString(nameObj);
            }
            catch (EcmaError ee)
            {
                if ("TypeError" == ee.getName())
                {
                    return false;
                }
                throw ee;
            }

            // See http://w3.org/TR/xml-names11/#NT-NCName
            int length = name.Length;
            if (length != 0)
            {
                if (isNCNameStartChar(name.ToCharArray()[0]))
                {
                    for (int i = 1; i != length; ++i)
                    {
                        if (!isNCNameChar(name.ToCharArray()[i]))
                        {
                            return false;
                        }
                    }
                    return true;
                }
            }

            return false;
        }

        private XmlNode.QName qname;
        private bool isAttributeName1;
        private bool isDescendants1;
        private XMLObjectImpl xmlObject;

        private XMLName()
        {
        }

        public static XMLName formStar()
        {
            XMLName rv = new XMLName();
            rv.qname = XmlNode.QName.create(null, null);
            return rv;
        }

        /** @deprecated */
        public static XMLName formProperty(XmlNode.Namespace namespace1, String localName)
        {
            if (localName != null && (localName == "*")) localName = null;
            XMLName rv = new XMLName();
            rv.qname = XmlNode.QName.create(namespace1, localName);
            return rv;
        }

        /** TODO: marked deprecated by original author */
        public static XMLName formProperty(String uri, String localName)
        {
            return formProperty(XmlNode.Namespace.create(uri), localName);
        }

        /** TODO: marked deprecated by original implementor */
        public static XMLName create(String defaultNamespaceUri, String name)
        {
            if (name == null)
                throw new ArgumentException();

            int l = name.Length;
            if (l != 0)
            {
                char firstChar = name.ToCharArray()[0];
                if (firstChar == '*')
                {
                    if (l == 1)
                    {
                        return XMLName.formStar();
                    }
                }
                else if (firstChar == '@')
                {
                    XMLName xmlName = XMLName.formProperty("", name.Substring(1));
                    xmlName.setAttributeName();
                    return xmlName;
                }
            }

            return XMLName.formProperty(defaultNamespaceUri, name);
        }

        public static XMLName create(XmlNode.QName qname, bool attribute, bool descendants)
        {
            XMLName rv = new XMLName();
            rv.qname = qname;
            rv.isAttributeName1 = attribute;
            rv.isDescendants1 = descendants;
            return rv;
        }

        /** @deprecated */
        public static XMLName create(XmlNode.QName qname)
        {
            return create(qname, false, false);
        }

        public void initXMLObject(XMLObjectImpl xmlObject)
        {
            if (xmlObject == null) throw new ArgumentException();
            if (this.xmlObject != null) throw new Exception();
            this.xmlObject = xmlObject;
        }

        public String uri()
        {
            if (qname.getNamespace() == null) return null;
            return qname.getNamespace().getUri();
        }

        public String localName()
        {
            if (qname.getLocalName() == null) return "*";
            return qname.getLocalName();
        }

        private void addDescendantChildren(XMLList list, XML target)
        {
            XMLName xmlName = this;
            if (target.isElement())
            {
                XML[] children = target.getChildren();
                for (int i = 0; i < children.Length; i++)
                {
                    if (xmlName.matches(children[i]))
                    {
                        list.addToList(children[i]);
                    }
                    addDescendantChildren(list, children[i]);
                }
            }
        }

        public void addMatchingAttributes(XMLList list, XML target)
        {
            XMLName name = this;
            if (target.isElement())
            {
                XML[] attributes = target.getAttributes();
                for (int i = 0; i < attributes.Length; i++)
                {
                    if (name.matches(attributes[i]))
                    {
                        list.addToList(attributes[i]);
                    }
                }
            }
        }

        private void addDescendantAttributes(XMLList list, XML target)
        {
            if (target.isElement())
            {
                addMatchingAttributes(list, target);
                XML[] children = target.getChildren();
                for (int i = 0; i < children.Length; i++)
                {
                    addDescendantAttributes(list, children[i]);
                }
            }
        }

        public XMLList matchDescendantAttributes(XMLList rv, XML target)
        {
            rv.setTargets(target, null);
            addDescendantAttributes(rv, target);
            return rv;
        }

        public XMLList matchDescendantChildren(XMLList rv, XML target)
        {
            rv.setTargets(target, null);
            addDescendantChildren(rv, target);
            return rv;
        }

        public void addDescendants(XMLList rv, XML target)
        {
            XMLName xmlName = this;
            if (xmlName.isAttributeName())
            {
                matchDescendantAttributes(rv, target);
            }
            else
            {
                matchDescendantChildren(rv, target);
            }
        }

        private void addAttributes(XMLList rv, XML target)
        {
            addMatchingAttributes(rv, target);
        }

        public void addMatches(XMLList rv, XML target)
        {
            if (isDescendants())
            {
                addDescendants(rv, target);
            }
            else if (isAttributeName())
            {
                addAttributes(rv, target);
            }
            else
            {
                XML[] children = target.getChildren();
                if (children != null)
                {
                    for (int i = 0; i < children.Length; i++)
                    {
                        if (this.matches(children[i]))
                        {
                            rv.addToList(children[i]);
                        }
                    }
                }
                rv.setTargets(target, this.toQname());
            }
        }

        public XMLList getMyValueOn(XML target)
        {
            XMLList rv = target.newXMLList();
            addMatches(rv, target);
            return rv;
        }

        public void setMyValueOn(XML target, Object value)
        {
            // Special-case checks for undefined and null
            if (value == null)
            {
                value = "null";
            }
            else if (value is Undefined)
            {
                value = "undefined";
            }

            XMLName xmlName = this;
            // Get the named property
            if (xmlName.isAttributeName())
            {
                target.setAttribute(xmlName, value);
            }
            else if (xmlName.uri() == null && (xmlName.localName() == "*"))
            {
                target.setChildren(value);
            }
            else
            {
                // Convert text into XML if needed.
                XMLObjectImpl xmlValue = null;

                if (value is XMLObjectImpl)
                {
                    xmlValue = (XMLObjectImpl)value;

                    // Check for attribute type and convert to textNode
                    if (xmlValue is XML)
                    {
                        if (((XML)xmlValue).isAttribute())
                        {
                            xmlValue = target.makeXmlFromString(xmlName,
                                    xmlValue.toString());
                        }
                    }

                    if (xmlValue is XMLList)
                    {
                        for (int i = 0; i < xmlValue.length(); i++)
                        {
                            XML xml = ((XMLList)xmlValue).item(i);

                            if (xml.isAttribute())
                            {
                                ((XMLList)xmlValue).replace(i, target.makeXmlFromString(xmlName, xml.toString()));
                            }
                        }
                    }
                }
                else
                {
                    xmlValue = target.makeXmlFromString(xmlName, ScriptRuntime.toString(value));
                }

                XMLList matches = target.getPropertyList(xmlName);

                if (matches.length() == 0)
                {
                    target.appendChild(xmlValue);
                }
                else
                {
                    // Remove all other matches
                    for (int i = 1; i < matches.length(); i++)
                    {
                        target.removeChild(matches.item(i).childIndex());
                    }

                    // Replace first match with new value.
                    XML firstMatch = matches.item(0);
                    target.replace(firstMatch.childIndex(), xmlValue);
                }
            }
        }


        public override bool has(Context cx)
        {
            if (xmlObject == null)
            {
                return false;
            }
            return xmlObject.hasXMLProperty(this);
        }


        public override Object get(Context cx)
        {
            if (xmlObject == null)
            {
                throw ScriptRuntime.undefReadError(Undefined.instance,
                    ToString());
            }
            return xmlObject.getXMLProperty(this);
        }


        public override Object set(Context cx, Object value)
        {
            if (xmlObject == null)
            {
                throw ScriptRuntime.undefWriteError(Undefined.instance,
                    ToString(),
                    value);
            }
            // Assignment to descendants causes parse error on bad reference
            // and this should not be called
            if (isDescendants1) throw Kit.codeBug();
            xmlObject.putXMLProperty(this, value);
            return value;
        }


        public override bool delete(Context cx)
        {
            if (xmlObject == null)
            {
                return true;
            }
            xmlObject.deleteXMLProperty(this);
            return !xmlObject.hasXMLProperty(this);
        }


        public override String ToString()
        {
            //return qname.localName();
            StringBuilder buff = new StringBuilder();
            if (isDescendants1) buff.Append("..");
            if (isAttributeName1) buff.Append('@');
            if (uri() == null)
            {
                buff.Append('*');
                if (localName() == "*")
                {
                    return buff.ToString();
                }
            }
            else
            {
                buff.Append('"').Append(uri()).Append('"');
            }
            buff.Append(':').Append(localName());
            return buff.ToString();
        }

        public XmlNode.QName toQname()
        {
            return this.qname;
        }

        public bool matchesLocalName(String localName1)
        {
            return (localName() == "*") || (localName() == localName1);
        }

        public bool matchesElement(XmlNode.QName qname)
        {
            if (this.uri() == null || (this.uri() == qname.getNamespace().getUri()))
            {
                if ((this.localName() == "*") || (this.localName() == qname.getLocalName()))
                {
                    return true;
                }
            }
            return false;
        }

        public bool matches(XML node)
        {
            XmlNode.QName qname = node.getNodeQname();
            String nodeUri = null;
            if (qname.getNamespace() != null)
            {
                nodeUri = qname.getNamespace().getUri();
            }
            if (isAttributeName1)
            {
                if (node.isAttribute())
                {
                    if (this.uri() == null || (this.uri() == nodeUri))
                    {
                        if ((this.localName() == "*") || (this.localName() == qname.getLocalName()))
                        {
                            return true;
                        }
                    }
                    return false;
                }
                else
                {
                    //    TODO    Could throw exception maybe, should not call this method on attribute name with arbitrary node type
                    //            unless we traverse all attributes and children habitually
                    return false;
                }
            }
            else
            {
                if (this.uri() == null || ((node.isElement()) && (this.uri() == nodeUri)))
                {
                    if (localName() == "*") return true;
                    if (node.isElement())
                    {
                        if (localName() == qname.getLocalName()) return true;
                    }
                }
                return false;
            }
        }

        /* TODO: marked deprecated by original author */
        public bool isAttributeName()
        {
            return isAttributeName1;
        }

        // TODO Fix whether this is an attribute XMLName at construction?
        // Marked deprecated by original author
        public void setAttributeName()
        {
            //        if (isAttributeName) throw new IllegalStateException();
            isAttributeName1 = true;
        }

        /* TODO: was marked deprecated by original author */
        public bool isDescendants()
        {
            return isDescendants1;
        }

        //    TODO    Fix whether this is an descendant XMLName at construction?
        /** @deprecated */
        public void setIsDescendants()
        {
            //        if (isDescendants) throw new IllegalStateException();
            isDescendants1 = true;
        }
    }// end for class
}
